This tutorial will take you through a complete attack on an encrypted bootloader using AES-256. This demonstrates how to use side-channel power analysis on practical systems, along with discussing how to perform analysis with different Analyzer models.
SCOPETYPE = 'OPENADC'
PLATFORM = 'CWLITEARM'
In the world of microcontrollers, a bootloader is a special piece of firmware that is made to let the user upload new programs into memory. This is especially useful for devices with complex code that may need to be patched or otherwise updated in the future - a bootloader makes it possible for the user to upload a patched version of the firmware onto the micro. The bootloader receives information from a communication line (a USB port, serial port, ethernet port, WiFi connection, etc...) and stores this data into program memory. Once the full firmware has been received, the micro can happily run its updated code.
There is one big security issue to worry about with bootloaders. A company may want to stop their customers from writing their own firmware and uploading it onto the micro. For example, this might be for protection reasons - hackers might be able to access parts of the device that weren't meant to be accessed. One way of stopping this is to add encryption. The company can add their own secret signature to the firmware code and encrypt it with a secret key. Then, the bootloader can decrypt the incoming firmware and confirm that the incoming firmware is correctly signed. Users will not know the secret key or the signature tied to the firmware, so they won't be able to "fake" their own.
This tutorial will work with a simple AES-256 bootloader. The victim will receive data through a serial connection, decrypt the command, and confirm that the included signature is correct. Then, it will only save the code into memory if the signature check succeeded. To make this system more robust against attacks, the bootloader will use cipher-block chaining (CBC mode). Our goal is to find the secret key and the CBC initialization vector so that we could successfully fake our own firmware.
The bootloader's communications protocol operates over a serial port at 38400 baud rate. The bootloader is always waiting for new data to be sent in this example; in real life one would typically force the bootloader to enter through a command sequence.
Commands sent to the bootloader look as follows:
|<-------- Encrypted block (16 bytes) ---------->|
| |
+------+------+------+------+------+------+ .... +------+------+------+
| 0x00 | Signature (4 Bytes) | Data (12 Bytes) | CRC-16 |
+------+------+------+------+------+------+ .... +------+------+------+
This frame has four parts:
0x00: 1 byte of fixed headerAs described in the diagram, the 16 byte block is not sent as plaintext. Instead, it is encrypted using AES-256 in CBC mode. This encryption method will be described in the next section.
The bootloader responds to each command with a single byte indicating if the CRC-16 was OK or not:
+------+
CRC-OK: | 0xA1 |
+------+
+------+
CRC Failed: | 0xA4 |
+------+
Then, after replying to the command, the bootloader veries that the signature is correct. If it matches the expected manufacturer's signature, the 12 bytes of data will be written to flash memory. Otherwise, the data is discarded.
The system uses the AES algorithm in Cipher Block Chaining (CBC) mode. In general one avoids using encryption 'as-is' (i.e. Electronic Code Book), since it means any piece of plaintext always maps to the same piece of ciphertext. Cipher Block Chaining ensures that if you encrypted the same thing a bunch of times it would always encrypt to a new piece of ciphertext.
You can see another reference on the design of the encryption side; we'll be only talking about the decryption side here. In this case AES-256 CBC mode is used as follows, where the details of the AES-256 Decryption block will be discussed in detail later:

This diagram shows that the output of the decryption is no longer used directly as the plaintext. Instead, the output is XORed with a 16 byte mask, which is usually taken from the previous ciphertext. Also, the first decryption block has no previous ciphertext to use, so a secret initialization vector (IV) is used instead. If we are going to decrypt the entire ciphertext (including block 0) or correctly generate our own ciphertext, we'll need to find this IV along with the AES key.
The system in this tutorial uses AES-256 encryption, which has a 256 bit (32 byte) key - twice as large as the 16 byte key we've attacked in previous tutorials. This means that our regular AES-128 CPA attacks won't quite work. However, extending these attacks to AES-256 is fairly straightforward: the theory is explained in detail in Extending AES-128 Attacks to AES-256.
As the theory page explains, our AES-256 attack will have 4 steps:
For this tutorial, we'll be using the bootloader-aes256 project, which we'll build as usual:
%%bash -s "$PLATFORM"
cd ../hardware/victims/firmware/bootloader-aes256
make PLATFORM=$1 CRYPTO_TARGET=NONE
To start, we'll proceed with setup as usual:
%run "Helper_Scripts/Setup.ipynb"
fw_path = "../hardware/victims/firmware/bootloader-aes256/bootloader-aes256-{}.hex".format(PLATFORM)
cw.programTarget(scope, prog, fw_path)
The next step we'll need to take in attacking this target is to communicate with it. Most of the transmission is fairly straight forward, but the CRC is a little tricky. Luckily, there's a lot of open source out there for calculating CRCs. In this case, we'll pull some code from pycrc:
# Class Crc
#############################################################
# These CRC routines are copy-pasted from pycrc, which are:
# Copyright (c) 2006-2013 Thomas Pircher <tehpeh@gmx.net>
#
class Crc(object):
"""
A base class for CRC routines.
"""
def __init__(self, width, poly):
"""The Crc constructor.
The parameters are as follows:
width
poly
reflect_in
xor_in
reflect_out
xor_out
"""
self.Width = width
self.Poly = poly
self.MSB_Mask = 0x1 << (self.Width - 1)
self.Mask = ((self.MSB_Mask - 1) << 1) | 1
self.XorIn = 0x0000
self.XorOut = 0x0000
self.DirectInit = self.XorIn
self.NonDirectInit = self.__get_nondirect_init(self.XorIn)
if self.Width < 8:
self.CrcShift = 8 - self.Width
else:
self.CrcShift = 0
def __get_nondirect_init(self, init):
"""
return the non-direct init if the direct algorithm has been selected.
"""
crc = init
for i in range(self.Width):
bit = crc & 0x01
if bit:
crc ^= self.Poly
crc >>= 1
if bit:
crc |= self.MSB_Mask
return crc & self.Mask
def bit_by_bit(self, in_data):
"""
Classic simple and slow CRC implementation. This function iterates bit
by bit over the augmented input message and returns the calculated CRC
value at the end.
"""
# If the input data is a string, convert to bytes.
if isinstance(in_data, str):
in_data = [ord(c) for c in in_data]
register = self.NonDirectInit
for octet in in_data:
for i in range(8):
topbit = register & self.MSB_Mask
register = ((register << 1) & self.Mask) | ((octet >> (7 - i)) & 0x01)
if topbit:
register ^= self.Poly
for i in range(self.Width):
topbit = register & self.MSB_Mask
register = ((register << 1) & self.Mask)
if topbit:
register ^= self.Poly
return register ^ self.XorOut
bl_crc = Crc(width = 16, poly=0x1021)
Now we can easily get the CRC for our message by calling bl_crc.bit_by_bit(message).
With that done, we can start communicating with the bootloader. Recall that the bootloader expects:
0x00We don't really care what the 16 byte message is (just that each is different so that we get a variety of hamming weights), so we'll use the same text/key module from earlier attacks.
We can now run the following block, and we should get 0xA4 back. You may need to run this block a few times to get the right response back.
import time
okay = 0
reset_target(scope)
while not okay:
target.ser.write('\0xxxxxxxxxxxxxxxxxx')
time.sleep(0.05)
num_char = target.ser.inWaiting()
response = target.ser.read(num_char)
if response:
print(response)
if ord(response[0]) == 0xA1:
okay = 1